package com.lambkit.plugin.activerecord;

import cn.hutool.core.lang.func.VoidFunc1;
import cn.hutool.core.util.StrUtil;
import cn.hutool.db.Db;
import cn.hutool.db.Entity;
import cn.hutool.db.transaction.TransactionLevel;
import cn.hutool.log.StaticLog;
import com.jfinal.plugin.activerecord.*;
import com.lambkit.db.*;
import com.lambkit.db.dialect.IDialect;
import com.lambkit.db.sql.Columns;
import com.lambkit.db.sql.Example;

import java.sql.Connection;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

/**
 * @author yangyong(孤竹行)
 */
public abstract class JFinalDbOpt<T extends IRowData, P extends IPageData<T>> extends AbstractDbOpt<T, P> {
    private DbPro jfDb;
    private IDialect dialect;

    public JFinalDbOpt(DbPro jfDb, IDialect dialect)  {
        super(dialect);
        this.jfDb = jfDb;
    }
    public DbPro getDb() {
        return jfDb;
    }

    public Config getConfig() {
        return jfDb.getConfig();
    }

    public Record toEntity(T rowData) {
        Record row = new Record();
        row.setColumns(rowData.toMap());
        return row;
    }

    private List<Record> toEntity( List<T> rowDatas) {
        List<Record> entities = new ArrayList<>(rowDatas.size());
        for(T rowData : rowDatas) {
            entities.add(toEntity(rowData));
        }
        return entities;
    }

    //public abstract P paginate(Integer pageNumber, Integer pageSize, Sql sqlPara);

    @Override
    public Long count(Sql sql) {
        Number number = jfDb.queryNumber(sql.getSql(), sql.getPara());
        return number != null ? number.longValue() : 0;
    }

    @Override
    public boolean save(T rowData) {
        Record entity = toEntity(rowData);
        boolean flag = jfDb.save(rowData.tableName(), rowData.primaryKey(), entity);
        if(flag) {
            rowData.put(entity.toMap());
        }
        return flag;
    }

    @Override
    public boolean delete(T rowData) {
        return jfDb.delete(rowData.tableName(), rowData.primaryKey(), toEntity(rowData));
    }

    @Override
    public boolean update(T rowData) {
        Record entity = toEntity(rowData);
        boolean flag = jfDb.update(rowData.tableName(), rowData.primaryKey(), toEntity(rowData));
        if(flag) {
            rowData.put(entity.toMap());
        }
        return flag;
    }

    @Override
    public int update(String sql, Object... paras) {
        return jfDb.update(sql, paras);
    }

    //public abstract List<T> find(String sql, Object... paras);

    @Override
    public int delete(String sql, Object... paras) {
        return jfDb.delete(sql, paras);
    }

    //public abstract boolean tx(Integer transactionLevel, ITxAction<V> atom);


    @Override
    public <V extends IDbOpt> boolean tx(Integer transactionLevel, ITxAction<V> atom) {
        return tx(getConfig(), getConfig().getTransactionLevel(), atom);
    }

    /**
     * Execute transaction.
     * @param config the Config object
     * @param transactionLevel the transaction level
     * @param atom the atom operation
     * @return true if transaction executing succeed otherwise false
     */
    protected boolean tx(Config config, int transactionLevel, ITxAction atom) {
        Connection conn = config.getThreadLocalConnection();
        if (conn != null) {	// Nested transaction support
            try {
                if (conn.getTransactionIsolation() < transactionLevel) {
                    conn.setTransactionIsolation(transactionLevel);
                }
                boolean result = atom.execute(this);
                if (result) {
                    return true;
                }
                throw new NestedTransactionHelpException("Notice the outer transaction that the nested transaction return false");	// important:can not return false
            }
            catch (SQLException e) {
                throw new ActiveRecordException(e);
            }
        }

        Boolean autoCommit = null;
        try {
            conn = config.getConnection();
            autoCommit = conn.getAutoCommit();
            config.setThreadLocalConnection(conn);
            conn.setTransactionIsolation(transactionLevel);
            conn.setAutoCommit(false);
            boolean result = atom.execute(this);
            if (result) {
                conn.commit();
            } else {
                conn.rollback();
            }
            return result;
        } catch (NestedTransactionHelpException e) {
            if (conn != null) {
                try {conn.rollback();} catch (Exception e1) {
                    StaticLog.error(e1.getMessage(), e1);}
            }
            return false;
        } catch (Throwable t) {
            if (conn != null) {
                try {conn.rollback();} catch (Exception e1) {StaticLog.error(e1.getMessage(), e1);}
            }
            throw t instanceof RuntimeException ? (RuntimeException)t : new ActiveRecordException(t);
        } finally {
            try {
                if (conn != null) {
                    if (autoCommit != null) {
                        conn.setAutoCommit(autoCommit);
                    }
                    conn.close();
                }
            } catch (Throwable t) {
                StaticLog.error(t.getMessage(), t);	// can not throw exception here, otherwise the more important exception in previous catch block can not be thrown
            } finally {
                config.removeThreadLocalConnection();	// prevent memory leak
            }
        }
    }

    @Override
    public int[] batch(String sql, Object[][] paras, int batchSize) {
        return jfDb.batch(sql, paras, batchSize);
    }

    @Override
    public int[] batch(List<String> sqlList, int batchSize) {
        return jfDb.batch(sqlList, batchSize);
    }

    @Override
    public int[] batchSave(List<T> recordList, int batchSize) {
        if(recordList == null || recordList.size() == 0) {
            return new int[0];
        }
        T rowData = recordList.get(0);
        return jfDb.batchSave(rowData.tableName(), toEntity(recordList), batchSize);
    }

    @Override
    public int[] batchUpdate(List<T> recordList, int batchSize) {
        if(recordList == null || recordList.size() == 0) {
            return new int[0];
        }
        T rowData = recordList.get(0);
        return jfDb.batchUpdate(rowData.tableName(), rowData.primaryKey(), toEntity(recordList), batchSize);
    }
}
